import ast
import datetime
import importlib
import json
import os
import re
import sys
import schedule
import time
from functools import wraps
import one_runtime
import util

# 脚本目录
job_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "jobs")


# 任务上下文
class JobCtx():

    def __init__(self, id: str = None, weeks: int = None, time_str: str = None):
        '''
        :param id: 脚本名_函数名 唯一
        :param weeks: 每周几列表
        :param time_str: 触发每天中的时间 或者 每xx分钟
        '''
        self.id = id
        self.weeks = weeks
        self.time_str = time_str

    # 判断是否是每xx分钟轮转的任务
    def is_min_rotation(self):
        return self.time_str and self.time_str.isdigit() and self.time_str.isdigit() > 0

    @classmethod
    def current_day(cls):
        today = datetime.datetime.today().date()
        return today.strftime("%Y%m%d")

    # 查看命中的是周几
    def point_week(self):
        now_week = datetime.datetime.now().weekday() + 1
        return now_week if now_week in self.weeks else None

    def to_dict(self):
        return {
            "id": self.id,
            "weeks": self.weeks,
            "time_str": self.time_str
        }

    def to_json(self):
        return json.dumps(self.to_dict(), ensure_ascii=False,default=str)

    # 实现 __repr__ 方法
    def __repr__(self):
        return self.to_json()


# 这是一个装饰器，用于注册定时任务
def sjob(desc: str = None, times: list[str] = None, weeks: list[int] = None, success_tell_dingding: bool = False):
    '''
    :param desc: 任务描述
    :param times: 每天里的时间，数组格式  00:00 - 23:59 如果想配置每天的每xx分钟执行，则填写单个元素数字["10"]代表每10分钟执行一次
    :param weeks: 每周几，数组格式 1-7
    :return:
    '''

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 设置上下文ID
            task_desc = desc if desc is not None else func.__name__
            run_job_id = str(time.time())
            ctx: JobCtx = args[0]
            error = None
            ret = None
            try:
                # 如果是每几分钟手动验证周几
                if ctx.is_min_rotation() and not ctx.point_week():
                    #     过滤执行
                    util.log('filter_trigger_job', {
                        'desc': task_desc,
                        'ctx': ctx
                    })
                    return None
                util.log('begin_trigger_job', {
                    'desc': task_desc,
                    'ctx': ctx
                })
                util.log_local.run_data = {
                    "run_job_id": run_job_id
                }
                # 调用实际的任务函数
                ret = func(*args, **kwargs)
                return ret
            except Exception as e2:
                error = e2
                raise e2
            finally:
                util.log('end_trigger_job', {
                    'desc': task_desc,
                    'ctx': ctx,
                    'error': error,
                    'ret': ret
                })
                # 群通知
                if success_tell_dingding or error:
                    util.tell_dingding({
                        "msgtype": "text",
                        "text": {
                            "content": f'''任务触发通知\n任务：{task_desc}\n触发规则：{ctx}\n触发id：{run_job_id}\n状态：{'已触发' if error is None else ('触发失败,' + util.error_stack(error))}\n'''
                        }
                    })
                # 清理线程局部变量
                if hasattr(util.log_local, "run_data"):
                    del util.log_local.run_data

        return wrapper

    return decorator


if sys.version_info < (3, 9):
    import astor

    unparse = astor.to_source
else:
    unparse = ast.unparse


class JobAnnotationAnalyzer(ast.NodeVisitor):
    def __init__(self, job_group):
        self.dependencies = set()
        self.jobs = {}
        self.job_group = job_group

    def visit_Import(self, node: ast.Import):
        for alias in node.names:
            self.dependencies.add(alias.name)
        self.generic_visit(node)

    def visit_FunctionDef(self, node: ast.FunctionDef):
        has_sjob = False
        desc = node.name
        times = []
        weeks = []
        for dec in node.decorator_list:
            if isinstance(dec, ast.Call) and dec.func.id == 'sjob':
                has_sjob = True
                for kw in dec.keywords:
                    if kw.arg == 'desc':
                        desc = kw.value.s
                    elif kw.arg == 'times':
                        times = [arg.s for arg in kw.value.elts]
                    elif kw.arg == 'weeks':
                        weeks = [int(arg.s) for arg in kw.value.elts]

        if not has_sjob:
            return
        if not re.compile('^[a-zA-Z_0-9]+$').match(node.name):
            raise ValueError(f"Function {node.name} must be [a-zA-Z_0-9]+")
        if len(node.args.args) != 1:
            raise ValueError(f"Function {node.name} must have exactly one arguments.")

        if not times:
            raise ValueError(f"Function {node.name} must have weeks or times")

        # 验证weeks
        if not weeks:
            weeks = [1, 2, 3, 4, 5, 6, 7]
        elif (min(weeks) < 1 or max(weeks) > 7 or len(weeks) != len(set(weeks))):
            raise ValueError(f"Function {node.name} weeks must be integers from 1 to 7 and should not repeat")

        # 验证times
        if len(times) == 1 and times[0].isdigit() and times[0].isdigit() > 0:
            #     仅有一个元素且是正整数当做是每xx分钟执行一次
            pass
        else:
            time_pattern = re.compile('^([01]?[0-9]|2[0-3]):[0-5][0-9]$')
            for time_str in times:
                if not time_pattern.match(time_str):
                    raise ValueError(f"Function {node.name} times must be in format 'HH:MM' and should not repeat")
                if len(times) != len(set(times)):
                    raise ValueError(f"Function {node.name} times should not repeat")

        # Insert the function name and arguments into the function body
        arg1 = node.args.args[0].arg
        func_signature = f"def {node.name}({arg1}:JobCtx):"

        # Extract the function body
        body = unparse(node.body).strip()
        # Add a tab to the beginning of each line in the function body
        body = '\n'.join(['\t' + line for line in body.split('\n')])
        # 使用正则表达式匹配字符串中的 '''xxx\nxxx'''
        # 使用正则表达式匹配字符串中的 'xxx\nxxx'
        pattern = re.compile(r"'([^']*\\n[^']*)'")
        # 对匹配到的每个字符串进行处理
        body = pattern.sub(lambda m: "'''" + m.group(1).replace('\\n', '\n') + "'''", body)
        # Combine function signature with the body
        full_function = func_signature + '\n' + body

        self.jobs[node.name] = {
            'id': self.job_group + "#" + node.name,
            'body': full_function,
            'desc': desc,
            'weeks': weeks,
            'times': times
        }
        self.generic_visit(node)


def analyze_code(job_group, code: str):
    tree = ast.parse(code)
    analyzer = JobAnnotationAnalyzer(job_group)
    analyzer.visit(tree)
    return {
        'dependencies': list(analyzer.dependencies),
        'jobs': analyzer.jobs
    }


def process_python_code(job_group, python_code: str) -> dict:
    analysis = analyze_code(job_group, python_code)
    dependencies = analysis['dependencies']
    jobs = analysis['jobs']
    return {
        'job_group': job_group,
        'dependencies': dependencies,
        'jobs': jobs
    }


def scan_for_py_files(path):
    py_files = []
    for root, dirs, files in os.walk(path):
        for file in files:
            if file.endswith(".py"):
                py_files.append(os.path.join(root, file))
    return py_files


def parse_py_file(file_path):
    file_name = os.path.basename(file_path).split('.')[0]
    if file_name == '__init__':
        return None
    with open(file_path, "r") as file:
        content = file.read()
        return process_python_code(file_name, content)


def refresh_jobs():
    temp = {}
    try:
        util.log('begin_refresh_jobs')

        def create_job(weeks, time_str, job_id, module, func):
            ctx = JobCtx(id=job_id, weeks=weeks, time_str=time_str)
            return lambda: getattr(module, func)(ctx)

        # 清理现有所有任务
        schedule.clear()
        if not os.path.isdir(job_directory):
            util.log('job_init', {}, "The 'jobs' folder does not exist in the current directory.")
            return

        py_files = scan_for_py_files(job_directory)
        for py_file in py_files:
            job_group_obj = parse_py_file(py_file)
            if not job_group_obj or not job_group_obj.get('jobs'):
                continue
            job_group = job_group_obj['job_group']
            if job_group in temp:
                raise ValueError('同在同job_group脚本', job_group)
            # 使用字符串的 replace 方法，将 a 中的 b 替换为空字符串
            module_name = py_file.replace(job_directory, "", 1)

            # 移除 '.py' 后缀
            if module_name.endswith('.py'):
                module_name = module_name[:-3]
            module_name = 'job.jobs' + module_name.replace('/', ".")
            job_group_obj.update({
                'module_name': module_name
            })
            # 刷新
            try:
                module = importlib.import_module(module_name)
                importlib.reload(module)
                job_group_obj.update({
                    'module': module
                })
                # 注册新任务
                for id, job in job_group_obj.get('jobs').items():
                    times = job.get('times')
                    weeks = job.get('weeks')
                    # 如果是每分钟，则直接运行every任时
                    if len(times) == 1 and times[0].isdigit() and times[0].isdigit() > 0:
                        schedule.every(int(times[0])).minutes.do(create_job(weeks, times[0], job.get('id'), module, id))
                        continue

                    # 根据需要调度的时间来注册任务
                    for time_str in times:
                        for week in weeks:
                            if week == 1:
                                schedule.every().monday.at(time_str).do(create_job(weeks, time_str, job.get('id'), module, id))
                            elif week == 2:
                                schedule.every().tuesday.at(time_str).do(create_job(weeks, time_str, job.get('id'), module, id))
                            elif week == 3:
                                schedule.every().wednesday.at(time_str).do(create_job(weeks, time_str, job.get('id'), module, id))
                            elif week == 4:
                                schedule.every().thursday.at(time_str).do(create_job(weeks, time_str, job.get('id'), module, id))
                            elif week == 5:
                                schedule.every().friday.at(time_str).do(create_job(weeks, time_str, job.get('id'), module, id))
                            elif week == 6:
                                schedule.every().saturday.at(time_str).do(create_job(weeks, time_str, job.get('id'), module, id))
                            elif week == 7:
                                schedule.every().sunday.at(time_str).do(create_job(weeks, time_str, job.get('id'), module, id))
                    util.log('register_job', job)
                temp.update({
                    job_group: job_group_obj
                })
            except Exception as e:
                util.log('job_module_update_error', {
                    'error': e
                }, f"Error importing module {module_name}")  # 动态导入 失败
    finally:
        one_runtime.jobs = temp
        util.log('end_refresh_jobs')


# 启动一个线程来运行任务调度
def __open_job_trigger():
    '''
    开始任务调度
    :return:
    '''
    while True:
        if one_runtime.job_trigger:
            schedule.run_pending()
        time.sleep(1)


def pause_trigger():
    '''
    暂停任务调度
    :return:
    '''
    one_runtime.job_trigger = False


def start_trigger():
    '''
    开始任务调度
    :return:
    '''
    one_runtime.job_trigger = True

